summaryrefslogtreecommitdiff
path: root/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx')
-rw-r--r--app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx353
1 files changed, 353 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx
new file mode 100644
index 00000000..00c375a9
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-tree-view.tsx
@@ -0,0 +1,353 @@
+"use client";
+
+import * as React from "react";
+import { ChevronDown, ChevronRight, Minus, Plus } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Badge } from "@/components/ui/badge";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { DepartmentNode } from "@/lib/users/knox-service";
+import { getDomainLabel, getDomainColor } from "./domain-constants";
+
+interface DepartmentAssignment {
+ id: number;
+ departmentCode: string;
+ assignedDomain: string;
+ description?: string | null;
+}
+
+interface DepartmentTreeViewProps {
+ departments: DepartmentNode[];
+ selectedDepartments: string[];
+ onSelectionChange: (selected: string[]) => void;
+ assignments: DepartmentAssignment[];
+ className?: string;
+}
+
+interface TreeNodeProps {
+ node: DepartmentNode;
+ selectedDepartments: string[];
+ onToggle: (departmentCode: string) => void;
+ expandedNodes: Set<string>;
+ onExpandToggle: (departmentCode: string) => void;
+ assignments: DepartmentAssignment[];
+ level: number;
+}
+
+function TreeNode({
+ node,
+ selectedDepartments,
+ onToggle,
+ expandedNodes,
+ onExpandToggle,
+ assignments,
+ level
+}: TreeNodeProps) {
+ const isExpanded = expandedNodes.has(node.departmentCode);
+ const hasChildren = node.children.length > 0;
+
+ // 현재 부서에 할당된 도메인 찾기
+ const assignment = assignments.find(a => a.departmentCode === node.departmentCode);
+
+ // 현재 노드의 선택 상태 확인
+ const isSelected = selectedDepartments.includes(node.departmentCode);
+
+ // 하위 노드들 중 선택된 것이 있는지 확인 (부분 선택 상태 표시용)
+ const hasSelectedChildren = React.useMemo(() => {
+ if (!hasChildren) return false;
+
+ const getAllChildCodes = (dept: DepartmentNode): string[] => {
+ const codes: string[] = [];
+ dept.children.forEach(child => {
+ codes.push(child.departmentCode);
+ codes.push(...getAllChildCodes(child));
+ });
+ return codes;
+ };
+
+ const childCodes = getAllChildCodes(node);
+ return childCodes.some(code => selectedDepartments.includes(code));
+ }, [node, selectedDepartments, hasChildren]);
+
+ const handleToggle = () => {
+ onToggle(node.departmentCode);
+ };
+
+ const handleExpandToggle = () => {
+ if (hasChildren) {
+ onExpandToggle(node.departmentCode);
+ }
+ };
+
+ return (
+ <div className="select-none">
+ <div
+ className={cn(
+ "flex items-center gap-2 py-2 px-3 hover:bg-accent/50 rounded-md transition-colors",
+ (isSelected || (!isSelected && hasSelectedChildren)) && "bg-accent/20"
+ )}
+ style={{ marginLeft: `${level * 16}px` }}
+ >
+ {/* 확장/축소 버튼 */}
+ <div className="flex items-center justify-center w-5 h-5">
+ {hasChildren ? (
+ <Button
+ variant="ghost"
+ size="sm"
+ className="h-5 w-5 p-0 hover:bg-transparent"
+ onClick={handleExpandToggle}
+ >
+ {isExpanded ? (
+ <ChevronDown className="h-3 w-3" />
+ ) : (
+ <ChevronRight className="h-3 w-3" />
+ )}
+ </Button>
+ ) : null}
+ </div>
+
+ {/* 체크박스 */}
+ <div className="flex items-center">
+ <Checkbox
+ checked={isSelected}
+ onCheckedChange={handleToggle}
+ className={cn(
+ "h-4 w-4",
+ !isSelected && hasSelectedChildren && "[&>*:first-child]:opacity-50"
+ )}
+ />
+ </div>
+
+ {/* 부서 정보 */}
+ <div className="flex-1 min-w-0 cursor-pointer" onClick={handleToggle}>
+ <div className="flex items-center gap-2">
+ <span className={cn(
+ "font-medium truncate",
+ (isSelected || (!isSelected && hasSelectedChildren)) && "text-primary"
+ )}>
+ {node.departmentName || node.departmentCode}
+ </span>
+
+ {/* 할당된 도메인 표시 */}
+ {assignment && (
+ <Badge
+ className={cn(
+ "text-xs shrink-0",
+ getDomainColor(assignment.assignedDomain)
+ )}
+ variant="outline"
+ >
+ {getDomainLabel(assignment.assignedDomain)}
+ </Badge>
+ )}
+
+ {/* lowDepartmentYn 표시 */}
+ {node.lowDepartmentYn === 'T' && (
+ <Badge
+ variant="secondary"
+ className="text-xs shrink-0"
+ >
+ 하위
+ </Badge>
+ )}
+ </div>
+
+ {/* 부서 코드 */}
+ <div className="text-xs text-muted-foreground truncate">
+ {node.departmentCode}
+ {assignment?.description && (
+ <span className="ml-1">• {assignment.description}</span>
+ )}
+ </div>
+ </div>
+ </div>
+
+ {/* 하위 노드들 */}
+ {hasChildren && isExpanded && (
+ <div className="mt-1">
+ {node.children.map((child) => (
+ <TreeNode
+ key={child.departmentCode}
+ node={child}
+ selectedDepartments={selectedDepartments}
+ onToggle={onToggle}
+ expandedNodes={expandedNodes}
+ onExpandToggle={onExpandToggle}
+ assignments={assignments}
+ level={level + 1}
+ />
+ ))}
+ </div>
+ )}
+ </div>
+ );
+}
+
+export function DepartmentTreeView({
+ departments,
+ selectedDepartments,
+ onSelectionChange,
+ assignments,
+ className,
+}: DepartmentTreeViewProps) {
+ const [expandedNodes, setExpandedNodes] = React.useState<Set<string>>(new Set());
+
+ // 부서 토글 핸들러
+ const handleToggle = (departmentCode: string) => {
+ const findNode = (nodes: DepartmentNode[], code: string): DepartmentNode | null => {
+ for (const node of nodes) {
+ if (node.departmentCode === code) return node;
+ const found = findNode(node.children, code);
+ if (found) return found;
+ }
+ return null;
+ };
+
+ const getAllChildCodes = (node: DepartmentNode): string[] => {
+ const codes: string[] = [];
+ node.children.forEach(child => {
+ codes.push(child.departmentCode);
+ codes.push(...getAllChildCodes(child));
+ });
+ return codes;
+ };
+
+ const targetNode = findNode(departments, departmentCode);
+ if (!targetNode) return;
+
+ const isCurrentlySelected = selectedDepartments.includes(departmentCode);
+
+ let newSelected: string[];
+ if (isCurrentlySelected) {
+ // 선택 해제: 해당 부서만 제거 (하위 부서는 유지, 상위 부서에도 영향 없음)
+ newSelected = selectedDepartments.filter(code => code !== departmentCode);
+ } else {
+ // 선택: 해당 부서 + 모든 하위 부서 추가
+ const childCodes = getAllChildCodes(targetNode);
+ const codesToAdd = [departmentCode, ...childCodes].filter(code => !selectedDepartments.includes(code));
+ newSelected = [...selectedDepartments, ...codesToAdd];
+ }
+
+ onSelectionChange(newSelected);
+ };
+
+ // 노드 확장/축소 핸들러
+ const handleExpandToggle = (departmentCode: string) => {
+ const newExpanded = new Set(expandedNodes);
+ if (newExpanded.has(departmentCode)) {
+ newExpanded.delete(departmentCode);
+ } else {
+ newExpanded.add(departmentCode);
+ }
+ setExpandedNodes(newExpanded);
+ };
+
+ // 전체 확장/축소
+ const handleExpandAll = () => {
+ if (expandedNodes.size === 0) {
+ const getAllCodes = (nodes: DepartmentNode[]): string[] => {
+ const codes: string[] = [];
+ nodes.forEach(node => {
+ if (node.children.length > 0) {
+ codes.push(node.departmentCode);
+ codes.push(...getAllCodes(node.children));
+ }
+ });
+ return codes;
+ };
+ setExpandedNodes(new Set(getAllCodes(departments)));
+ } else {
+ setExpandedNodes(new Set());
+ }
+ };
+
+ // 전체 선택/해제
+ const handleSelectAll = () => {
+ if (selectedDepartments.length === 0) {
+ // 전체 선택
+ const allCodes: string[] = [];
+ const collectCodes = (nodes: DepartmentNode[]) => {
+ nodes.forEach(node => {
+ allCodes.push(node.departmentCode);
+ collectCodes(node.children);
+ });
+ };
+ collectCodes(departments);
+ onSelectionChange(allCodes);
+ } else {
+ // 전체 해제
+ onSelectionChange([]);
+ }
+ };
+
+ return (
+ <div className={cn("border rounded-lg", className)}>
+ {/* 헤더 */}
+ <div className="flex items-center justify-between p-3 border-b bg-muted/30">
+ <h3 className="font-medium">조직도</h3>
+ <div className="flex gap-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleExpandAll}
+ className="text-xs"
+ >
+ {expandedNodes.size === 0 ? (
+ <>
+ <Plus className="mr-1 h-3 w-3" />
+ 전체 펼치기
+ </>
+ ) : (
+ <>
+ <Minus className="mr-1 h-3 w-3" />
+ 전체 접기
+ </>
+ )}
+ </Button>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleSelectAll}
+ className="text-xs"
+ >
+ {selectedDepartments.length === 0 ? "전체 선택" : "선택 해제"}
+ </Button>
+ </div>
+ </div>
+
+ {/* 트리 본문 */}
+ <ScrollArea className="h-[80vh] p-2">
+ {departments.length === 0 ? (
+ <div className="text-center py-8 text-muted-foreground">
+ 부서 정보가 없습니다
+ </div>
+ ) : (
+ <div className="space-y-1">
+ {departments.map((dept) => (
+ <TreeNode
+ key={dept.departmentCode}
+ node={dept}
+ selectedDepartments={selectedDepartments}
+ onToggle={handleToggle}
+ expandedNodes={expandedNodes}
+ onExpandToggle={handleExpandToggle}
+ assignments={assignments}
+ level={0}
+ />
+ ))}
+ </div>
+ )}
+ </ScrollArea>
+
+ {/* 푸터 */}
+ {selectedDepartments.length > 0 && (
+ <div className="border-t p-3 bg-muted/30">
+ <div className="text-sm text-muted-foreground">
+ 선택된 부서: <span className="font-medium text-foreground">{selectedDepartments.length}개</span>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+} \ No newline at end of file